/*
 * This file is part of Advanced ABX
 *
 * Copyright 2010 Christian Uebber
 *
 * Advanced ABX is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2, as
 * published by the Free Software Foundation.
 *
 * Advanced ABX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Advanced ABX.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.uebber.sound.lib;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.LineUnavailableException;
import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;

public class SwitchableArrayClip extends ArrayClip {

    private byte[][] audioData;

    private volatile int activeStream = 0;

    private Queue<Integer> pendingSwitches = new LinkedList<Integer>();

    private Transitioner transitioner;

    private int transitionLength = 300;

    private int maxOutputBufferLen;

    public SwitchableArrayClip() {
        transitioner = new OutInFader();
    }

    /**
     * Opens the clip with the format and audio data present in the provided audio
     * input stream and buffers it completely. Additional streams can be added with
     * <code>addStreams(AudioInputStream...)</code>.
     * <p/>
     * If this operation succeeds, the line is marked open and an
     * <code>{@link javax.sound.sampled.LineEvent.Type#OPEN OPEN}</code> event is dispatched
     * to the line's listeners.
     * <p/>
     * Invoking this method on a line which is already open is illegal
     * and may result in an IllegalStateException.
     * <p/>
     *
     * @param stream an audio input stream from which audio data will be read into
     *               the clip
     * @throws javax.sound.sampled.LineUnavailableException
     *                                  if the line cannot be
     *                                  opened due to resource restrictions
     * @throws java.io.IOException      if an I/O exception occurs during reading of
     *                                  the stream
     * @throws IllegalArgumentException if the stream's audio format
     *                                  is not fully specified or invalid
     * @throws SecurityException        if the line cannot be
     *                                  opened due to security restrictions
     * @see #close
     * @see #isOpen
     * @see #addStreams(javax.sound.sampled.AudioInputStream...)
     * @see javax.sound.sampled.LineListener
     */
    @Override
    public void open(AudioInputStream stream) throws LineUnavailableException, IOException {
 
        // Convert uncommon formats to signed PCM
        if (stream.getFormat().getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
            stream = AudioSystem.getAudioInputStream(AudioFormat.Encoding.PCM_SIGNED, stream);
        }

        frameLength = (int) stream.getFrameLength();
        format = stream.getFormat();
        frameSize = format.getFrameSize();

        // Initialize internal buffer
        int len = frameLength * frameSize;
        audioData = new byte[1][len];

        // Fill it
        int numRead = 0;
        int offset = 0;
        while (numRead != -1) {
            numRead = stream.read(audioData[0], offset - numRead, len - numRead);
        }

        setLoopPoints(0, -1);
        framePos = 0;

        // Don't allow output buffers to pile up more than 50 ms of audio
        maxOutputBufferLen = (int) (format.getSampleRate() / 20) * frameSize;

        active = false;

        // Acquire and open SourceDataLine
        dataLine = AudioSystem.getSourceDataLine(format);
        dataLine.open(format);
    }

    /**
     * Adds and buffers additional streams, which will be ready for switching afterwards.
     *
     * @param streams One or more AudioInputStreams
     * @see #switchStream(int)
     */
    public synchronized void addStreams(AudioInputStream... streams) throws IOException {
        if (!isOpen())
            throw new IllegalStateException("Clip must be opened first!");

        // Create new Object array to hold references to existing and new audio data
        byte[][] temp = new byte[audioData.length + streams.length][];

        // Reference existing data
        for (int i = 0; i < audioData.length; i++) {
            temp[i] = audioData[i];
        }

        // Validate and read data into memory
        for (int i = 0; i < streams.length; i++) {
            // Convert uncommon formats to signed PCM
            if (streams[i].getFormat().getEncoding() != AudioFormat.Encoding.PCM_SIGNED) {
                streams[i] = AudioSystem.getAudioInputStream(AudioFormat.Encoding.PCM_SIGNED, streams[i]);
            }
            // Assure identical audio format and length
            if (!streams[i].getFormat().matches(format) || streams[i].getFrameLength() != frameLength)
                throw new IllegalArgumentException(
                        "Audio format and length must be identical to the first audio stream!");

            int len = frameLength * frameSize;

            // Allocate memory for stream i
            temp[audioData.length + i] = new byte[len];

            // Fill it
            int numRead = 0;
            int offset = 0;
            while (numRead != -1) {
                numRead = streams[i].read(temp[audioData.length + i], offset - numRead, len - numRead);
            }
        }
        audioData = temp;
    }

    /**
     * Switches to <code>streamNumber</code> using the current Transitioner.
     *
     * @param streamNumber Stream number to switch to.
     */
    public synchronized void switchStream(int streamNumber) {
        if (streamNumber >= audioData.length || streamNumber < 0) {
            throw new IllegalArgumentException("Stream number " + streamNumber + " does not exist!");
        } else if (!active && pendingSwitches.isEmpty()) {
            // Switch without transition
            activeStream = streamNumber;
        } else {
            // Schedule transition
            pendingSwitches.add(streamNumber);
        }
    }

    /**
     * Allows a line to engage in data I/O.  If invoked on a line
     * that is already running, this method does nothing.  Unless the data in
     * the buffer has been flushed, the line resumes I/O starting
     * with the first frame that was unprocessed at the time the line was
     * stopped. When audio capture or playback starts, a
     * <code>{@link javax.sound.sampled.LineEvent.Type#START START}</code> event is generated.
     *
     * @see #stop()
     * @see #isRunning()
     * @see javax.sound.sampled.LineEvent
     */
    @Override
    public void start() {

        if (active)
            return;

        new Thread() {
            @Override
            public void run() {
                dataLine.start();

                active = true;

                do {
                    timeStarted = System.currentTimeMillis() * 1000 - framesToMicroseconds(getFramePosition());

                    while (active &&
                            ((loopCount > 0 && framePos <= loopEnd) || (loopCount == 0 && framePos < frameLength))) {

                        Integer nextStream;
                        synchronized (this) {
                            // Check if there are pending switching requests
                            nextStream = pendingSwitches.poll();
                        }

                        if (nextStream != null) {
                            // A switch has been requested, process the transition
                            byte[] transitionBuffer = new byte[transitionLength * frameSize];
                            transitioner.process(audioData[activeStream], audioData[nextStream], framePos * frameSize,
                                    transitionBuffer, 0, transitionLength, format);
                            // Write transition to output buffer
                            framePos += dataLine.write(transitionBuffer, 0, transitionLength * frameSize) / frameSize;
//                            logger.info("Transition of " + framesToMicroseconds(transitionLength) / 1000
//                                    + " ms to stream " + nextStream + " at " + framePos);
                            activeStream = nextStream;
                        } else {
                            int buffered = dataLine.getBufferSize() - dataLine.available();
                            if (buffered >= maxOutputBufferLen)
                                try {
                                    sleep(20);
                                } catch (InterruptedException e) {
                                }
                            int bytePos = framePos * frameSize;
                            framePos += dataLine.write(audioData[activeStream], bytePos, Math.min(maxOutputBufferLen - buffered,
                                    (loopCount > 0 ? (loopEnd + 1) * frameSize : audioData[0].length) - bytePos)) / frameSize;
                        }
                    }

                    if (loopCount > 0) {
                        framePos = loopStart;
                    }
                } while (active && loopCount > 0 && --loopCount >= 0);

                active = false;
            }
        }.start();
    }

    public Transitioner getTransitioner() {
        return transitioner;
    }

    public void setTransitioner(Transitioner transitioner) {
        this.transitioner = transitioner;
    }

    public int getTransitionLength() {
        return transitionLength;
    }

    public void setTransitionLength(int transitionLength) {
        this.transitionLength = transitionLength;
    }

    /**
     * Closes the line, indicating that any system resources
     * in use by the line can be released.  If this operation
     * succeeds, the line is marked closed and a <code>CLOSE</code> event is dispatched
     * to the line's listeners.
     *
     * @throws SecurityException if the line cannot be
     *                           closed due to security restrictions.
     * @see #open
     * @see #isOpen
     * @see javax.sound.sampled.LineEvent
     */
    @Override
    public void close
            () {
        dataLine.close();
        audioData = null;
    }

//    private static final Logger logger = Logger.getLogger(SwitchableArrayClip.class.getName());
}
